<!doctype html>
<meta charset=utf-8>
<meta name="timeout" content="long">
<title>Support for all stats defined in WebRTC Stats</title>
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<script src="../webrtc/RTCPeerConnection-helper.js"></script>
<script src="/resources/WebIDLParser.js"></script>
<script>
'use strict';

// inspired from similar test for MTI stats in ../webrtc/RTCPeerConnection-mandatory-getStats.https.html

// From https://w3c.github.io/webrtc-stats/webrtc-stats.html#rtcstatstype-str*
const dictionaryNames = {
  "codec": "RTCCodecStats",
  "inbound-rtp": "RTCInboundRtpStreamStats",
  "outbound-rtp": "RTCOutboundRtpStreamStats",
  "remote-inbound-rtp": "RTCRemoteInboundRtpStreamStats",
  "remote-outbound-rtp": "RTCRemoteOutboundRtpStreamStats",
  "csrc": "RTCRtpContributingSourceStats",
  "peer-connection": "RTCPeerConnectionStats",
  "data-channel": "RTCDataChannelStats",
  "media-source": {
    audio: "RTCAudioSourceStats",
    video: "RTCVideoSourceStats"
  },
  "media-playout": "RTCAudioPlayoutStats",
  "sender": {
    audio: "RTCAudioSenderStats",
    video: "RTCVideoSenderStats"
  },
  "receiver": {
    audio: "RTCAudioReceiverStats",
    video: "RTCVideoReceiverStats",
  },
  "transport": "RTCTransportStats",
  "candidate-pair": "RTCIceCandidatePairStats",
  "local-candidate": "RTCIceCandidateStats",
  "remote-candidate": "RTCIceCandidateStats",
  "certificate": "RTCCertificateStats",
};

function isPropertyTestable(type, property) {
  // List of properties which are not testable by this test.
  // When adding something to this list, please explain why.
  const untestablePropertiesByType = {
    'candidate-pair': [
      'availableIncomingBitrate', // requires REMB, no TWCC.
    ],
    'certificate': [
      'issuerCertificateId', // we only use self-signed certificates.
    ],
    'local-candidate': [
      'url', // requires a STUN/TURN server.
      'relayProtocol', // requires a TURN server.
      'relatedAddress', // requires a STUN/TURN server.
      'relatedPort', // requires a STUN/TURN server.
    ],
    'remote-candidate': [
      'url', // requires a STUN/TURN server.
      'relayProtocol', // requires a TURN server.
      'relatedAddress', // requires a STUN/TURN server.
      'relatedPort', // requires a STUN/TURN server.
      'tcpType', // requires ICE-TCP connection.
    ],
    'outbound-rtp': [
      'rid', // requires simulcast.
    ],
    'inbound-rtp': [
      'fecSsrc', // requires FlexFEC to be negotiated.
    ],
    'media-source': [
      'echoReturnLoss', // requires gUM with an audio input device.
      'echoReturnLossEnhancement', // requires gUM with an audio input device.
    ]
  };
  if (!untestablePropertiesByType[type]) {
    return true;
  }
  return !untestablePropertiesByType[type].includes(property);
}

async function getAllStats(t, pc) {
  // Try to obtain as many stats as possible, waiting up to 20 seconds for
  // roundTripTime which can take several RTCP messages to calculate.
  let stats;
  for (let i = 0; i < 20; i++) {
    stats = await pc.getStats();
    const values = [...stats.values()];
    const [remoteInboundAudio, remoteInboundVideo] =
        ["audio", "video"].map(kind =>
            values.find(s => s.type == "remote-inbound-rtp" && s.kind == kind));
    const [remoteOutboundAudio, remoteOutboundVideo] =
        ["audio", "video"].map(kind =>
            values.find(s => s.type == "remote-outbound-rtp" && s.kind == kind));
    // We expect both audio and video remote-inbound-rtp RTT.
    const hasRemoteInbound =
        remoteInboundAudio && "roundTripTime" in remoteInboundAudio &&
        remoteInboundVideo && "roundTripTime" in remoteInboundVideo;
    // Due to current implementation limitations, we don't put as hard
    // requirements on remote-outbound-rtp as remote-inbound-rtp. It's enough if
    // it is available for either kind and `roundTripTime` is not required. In
    // Chromium, remote-outbound-rtp is only implemented for audio and
    // `roundTripTime` is missing in this test, but awaiting for any
    // remote-outbound-rtp avoids flaky failures.
    const hasRemoteOutbound = remoteOutboundAudio || remoteOutboundVideo;
    const hasMediaPlayout = values.find(({type}) => type == "media-playout") != undefined;
    if (hasRemoteInbound && hasRemoteOutbound && hasMediaPlayout) {
      return stats;
    }
    await new Promise(r => t.step_timeout(r, 1000));
  }
  return stats;
}

promise_test(async t => {
  // load the IDL to know which members to be looking for
  const idl = await fetch("/interfaces/webrtc-stats.idl").then(r => r.text());
  // for RTCStats definition
  const webrtcIdl = await fetch("/interfaces/webrtc.idl").then(r => r.text());
  const astArray = WebIDL2.parse(idl + webrtcIdl);

  let all = {};
  for (let type in dictionaryNames) {
      // TODO: make use of audio/video distinction
    let dictionaries = dictionaryNames[type].audio ? Object.values(dictionaryNames[type]) : [dictionaryNames[type]];
    all[type] = [];
    let i = 0;
    // Recursively collect members from inherited dictionaries
    while (i < dictionaries.length) {
      const dictName = dictionaries[i];
      const dict = astArray.find(i => i.name === dictName && i.type === "dictionary");
      if (dict && dict.members) {
        all[type] = all[type].concat(dict.members.map(m => m.name));
        if (dict.inheritance) {
          dictionaries.push(dict.inheritance);
        }
      }
      i++;
    }
    // Unique-ify
    all[type] = [...new Set(all[type])];
  }

  const remaining = JSON.parse(JSON.stringify(all));
  for (const type in remaining) {
    remaining[type] = new Set(remaining[type]);
  }

  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc2.close());

  const dc1 = pc1.createDataChannel("dummy", {negotiated: true, id: 0});
  const dc2 = pc2.createDataChannel("dummy", {negotiated: true, id: 0});
  // Use a real gUM to ensure that all stats exposing hardware capabilities are
  // also exposed.
  const stream = await navigator.mediaDevices.getUserMedia(
    {video: true, audio: true});
  for (const track of stream.getTracks()) {
    pc1.addTrack(track, stream);
    pc2.addTrack(track, stream);
    t.add_cleanup(() => track.stop());
  }

  // Do a non-trickle ICE handshake to ensure that TCP candidates are gathered.
  await pc1.setLocalDescription();
  await waitForIceGatheringState(pc1, ['complete']);
  await pc2.setRemoteDescription(pc1.localDescription);
  await pc2.setLocalDescription();
  await waitForIceGatheringState(pc2, ['complete']);
  await pc1.setRemoteDescription(pc2.localDescription);
  // Await the DTLS handshake.
  await Promise.all([
    listenToConnected(pc1),
    listenToConnected(pc2),
  ]);
  const stats = await getAllStats(t, pc1);

  // The focus of this test is that there are no dangling references,
  // i.e. keys ending with `Id` as described in
  // https://w3c.github.io/webrtc-stats/#guidelines-for-design-of-stats-objects
  test(t => {
    for (const stat of stats.values()) {
      Object.keys(stat).forEach(key => {
        if (!key.endsWith('Id')) return;
        assert_true(stats.has(stat[key]), `${stat.type}.${key} can be resolved`);
      });
    }
  }, 'All references resolve');

  // The focus of this test is not API correctness, but rather to provide an
  // accessible metric of implementation progress by dictionary member. We count
  // whether we've seen each dictionary's members in getStats().

  test(t => {
    for (const stat of stats.values()) {
      if (all[stat.type]) {
        const memberNames = all[stat.type];
        const remainingNames = remaining[stat.type];
        assert_true(memberNames.length > 0, "Test error. No member found.");
        for (const memberName of memberNames) {
          if (memberName in stat) {
            assert_not_equals(stat[memberName], undefined, "Not undefined");
            remainingNames.delete(memberName);
          }
        }
      }
    }
  }, "Validating stats");

  for (const type in all) {
    for (const memberName of all[type]) {
      test(t => {
        assert_implements_optional(isPropertyTestable(type, memberName),
          `${type}.${memberName} marked as not testable.`);
        assert_true(!remaining[type].has(memberName),
                    `Is ${memberName} present`);
      }, `${type}'s ${memberName}`);
    }
  }
}, 'getStats succeeds');
</script>
